package com.sysdep;

import java.util.List;
import com.ir.*;
import com.asm.*;
import com.ast.FuncDefNode;

public class CodeGenerator {

    public CodeGenerator() {
        // file = new CodeAssembly();
    }

    public CodeAssembly generate(IR ir) {
        locateGloableValues(ir);
        return generateCodeAssembly(ir);
    }

    private void locateGloableValues(IR ir) {
        for (Var var : ir.getVars()) {
            Symbol sym = namedSymbol(var.getName());
            var.setMemoryReference(sym, 0, true);
        }
    }

    private CodeAssembly generateCodeAssembly(IR ir) {
        CodeAssembly file = new CodeAssembly();
        generateGlobalVariables(file, ir.getVars());
        generateFunctions(file, ir.getFuns());
        return file;
    }

    private void generateGlobalVariables(CodeAssembly file, List<Var> vars) {
        file._data();
        for (Var var : vars) {
            var.Init();
            Symbol sym = namedSymbol(var.getName());
            if (var.isArray()) {
                if(var.getType() == "int")
                    generateGlobalArrayInt(file, sym, var);
                else
                    generateGlobalArrayFloat(file, sym, var);
                // TODO:array
            } else {
                if (var.getType() == "int")
                    generateGlobalInt(file, sym, var);
                else if (var.getType() == "float") {
                    generateGlobalFloat(file, sym, var);
                    // TODO:float
                } else {// error
                    return;
                }
            }
        }
    }

    public void generateGlobalArrayInt(CodeAssembly file, Symbol sym, Var var) {
        file._globl(sym);
        file._type(sym, LabelType.obj);
        file._size(sym, var.getArraySize() * 4);
        file.label(sym);
            for(int i = 0; i < var.getArraySize(); i++){
                if(i < var.getValue_list().size())
                    file._int(((Int) var.getValue_list().get(i)).getValue());
                else
                    file._int(0);
            }
    }

    public void generateGlobalArrayFloat(CodeAssembly file, Symbol sym, Var var) {
        file._globl(sym);
        file._type(sym, LabelType.obj);
        file._size(sym, var.getArraySize() * 4);
        file.label(sym);
            for(int i = 0; i < var.getArraySize(); i++){
                if(i < var.getValue_list().size())
                    file._float(((Int) var.getValue_list().get(i)).getValue());
                else
                    file._float(0);
            }
    }

    public void generateGlobalInt(CodeAssembly file, Symbol sym, Var var) {
        file._globl(sym);
        file._type(sym, LabelType.obj);
        file._size(sym, 4);
        file.label(sym);
        if (var.getExpr() == null) {
            file._int(0);
        }
        else
            file._int(((Int) var.getExpr()).getValue());
        // file._size(sym);
    }

    public void generateGlobalFloat(CodeAssembly file, Symbol sym, Var var) {
        file._globl(sym);
        file._type(sym, LabelType.obj);
        file._size(sym, 4);
        file.label(sym);
        if (var.getExpr() == null) {
            file._float(0);
        }
        else
            file._float(((FloatIR) var.getExpr()).getValue());
        // file._size(sym);
    }

    private Symbol namedSymbol(String name) {
        return (Symbol) new NamedSymbol(name);
    }

    private Symbol unnamedSymbol() {
        return (Symbol) new UnnamedSymbol();
    }

    // private long maxParamLen = 0;

    private void generateFunctions(CodeAssembly file, List<FuncDefNode> funcs) {
        file._text();
        for (FuncDefNode node : funcs) {
            Symbol sym = namedSymbol(node.getName());
            file._globl(sym);
            file._type(sym, LabelType.func);
            file.label(sym);
            generateFuncBody(file, node);
            file.addAll(as);
            file._size(sym);
        }
    }

    private long locateParameters(List<Var> params) {
        long offset = 4;
        for (Var param : params) {
            offset += 4;
        param.setMemoryReference(FP(), -offset);
        param.Init();
        }
        return offset;
    }

    private void loadParameters(List<Var> params) {
        for (int i = 0; i < params.size(); i++) {
            switch (i) {
                case 0:
                    as.str(R0(), params.get(0).getMemRef());
                    break;
                case 1:
                    as.str(R1(), params.get(1).getMemRef());
                    break;
                case 2:
                    as.str(R2(), params.get(2).getMemRef());
                    break;
                case 3:
                    as.str(R3(), params.get(3).getMemRef());
                    break;
                default:
                    as.ldr(R0(), new IndirectMemoryReference((i - 3) * 4, FP()));
                    as.str(R0(), params.get(i).getMemRef());
                    break;
            }
        }
    }

    private void loadFloatIRs(List<FloatIR> floatIRs) {
        as.label(fsym);
        for (FloatIR f : floatIRs) {
            as._float(f.getValue());
        }
    }

    private void loadGlobalVariablesInFunc(List<Var> vars) {
        for (Var var : vars) {
            as._word(var.getName());
        }
    }

    public long locateLocalVariables(List<Var> variables, long offset) {// no arrays
        for (Var var : variables) {
            if (!var.isArray()) 
                offset += 4;
            else
                offset += 4 * var.getArraySize();
            var.setMemoryReference(FP(), -offset);
        }
        return offset;
    }

    private long locateFloatIRs(List<FloatIR> floatIRs) {
        long offset = 0;
        for (FloatIR flt : floatIRs) {
            flt.setMemoryReference(fsym, offset);
            offset += 4;
        }
        return offset;
    }

    private void locateGlobalVarsInFunc(List<Var> vars, long offset) {
        for (Var var : vars) {
            var.setGlobal(true);
            var.setMemoryReference(fsym, offset, true);
            if(!var.isArray())
                offset += 4;
            else
                offset += 4 * var.getArraySize();
        }
    }

    private Register createRegister(RegisterType registerType) {
        return createRegister(registerType, Type.INT32);
    }

    private Register createRegister(RegisterType registerType, Type type) {
        return new Register(registerType, type);
    }

    private ImmediateValue imm(long value, boolean isAddress) {
        return new ImmediateValue(value, isAddress);
    }

    private CodeAssembly as;
    private Label epilogue;
    private Symbol fsym;

    private void generateFuncBody(CodeAssembly file, FuncDefNode node) {
        as = new CodeAssembly();
        epilogue = new Label();
        fsym = unnamedSymbol();
        as.push(FP(), LR());
        as.add(FP(), SP(), imm(4, false));
        long stackOffset = locateParameters(node.getFunc().getParams());
        stackOffset = locateLocalVariables(node.getFunc().getVars(), stackOffset);
        long floatOffset = locateFloatIRs(node.getFunc().getFloatIRs());
        locateGlobalVarsInFunc(node.getFunc().getGlobal(), floatOffset);
        int paramSize = node.getFunc().getParamsNum() - 4;
        if (paramSize < 0) {
            paramSize = 0;
        }
        stackOffset += paramSize * 4;
        if (stackOffset % 8 != 0) {
            stackOffset -= 4;
        }
        as.sub(SP(), SP(), imm(stackOffset, false));
        loadParameters(node.getFunc().getParams());
        List<Stmt> stmts = node.getFunc().getStmts();
        for (Stmt stmt : stmts) {
            if (stmt instanceof ExprStmt)
                ;
            compileStmt(stmt);
        }
        as.label(epilogue);
        as.sub(SP(), FP(), imm(4, false));
        as.pop(FP(), LR());
        as.ret(LR());
        loadFloatIRs(node.getFunc().getFloatIRs());
        loadGlobalVariablesInFunc(node.getFunc().getGlobal());
    }

    private void compileStmt(Stmt stmt) {
        stmt.accept(this);
    }

    public void visit(Return ret) {
        if (ret.getExpr() != null) {
            compile(ret.getExpr());
        }
        as.jmp(epilogue);
    }

    public boolean visit(Call call) {
        List<Expr> exprs = call.getExprs();
        boolean result = false;
        if (call.getFunc().getType().getType().equals("float")) {
            result = true;
        }
        for (int i = 1; i < exprs.size(); i++) {
            compile(exprs.get(i));
            switch (i) {
                case 1:
                    as.mov(R1(), R0());
                    break;
                case 2:
                    as.mov(R2(), R0());
                    break;
                case 3:
                    as.mov(R3(), R0());
                    break;
                default:
                    as.str(R0(), new IndirectMemoryReference((i - 4) * 4, SP()));
                    break;
            }
        }
        if(exprs.size() > 0)
            compile(exprs.get(0));
        Symbol sym = namedSymbol(call.getFunc().getName());
        as.call(new DirectMemoryReference(sym));
        return result;
    }

    public void visit(Jump jump) {
        as.jmp(jump.getLabel());
    }

    public void visit(CJump cJump) {
        compile(cJump.getCond());
        as.cmp(R0(), imm(0, false));
        as.jmpne(cJump.getThenLabel());
        as.jmp(cJump.getElseLabel());
    }

    public void visit(ExprStmt expr) {
        if (expr.getExpr() == null) {
            return;
        }
        compile(expr.getExpr());
    }

    public void visit(LabelStmt labelStmt) {
        as.label(labelStmt.getLabel());
    }

    private boolean compile(Expr expr) {
        return expr.accept(this);
    }

    public boolean visit(Int integer) {
        as.ldr(R0(), imm(integer.getValue(), true));
        return false;
    }

    public boolean visit(FloatIR flt) {
        as.ldr(R0(), flt.getReference());
        return true;
    }

    public boolean visit(UnaryExp uni) {
        boolean result = compile(uni.getExp());
        switch (uni.getOp()) {
            case "-":
                as.mov(R1(), imm(0, false));
                as.sub(R0(), R1(), R0());
                break;
            case "!":
                as.mov(R1(), imm(0, false));
                as.cmp(R0(), R1());
                as.mov(R0(), imm(0, false));
                as.moveq(R0(), imm(1, false));
                break;
            default:
                break;
        }
        return result;
    }

    public boolean visit(BinExp bin) {
        boolean result, res1, res2;
        if(bin.getOp().equals("||")){
            res1 = compile(bin.getLhs());
                as.mov(R1(), R0());
                Label end2 = new Label();
                as.mov(R0(), imm(0, false));
                as.cmp(R1(), imm(0, false));
                as.movne(R0(), imm(1, false));
                as.cmp(R0(), imm(0, false));
                as.bne(end2);
                res2 = compile(bin.getRhs());
                as.mov(R2(), R0());
                as.cmp(R2(), imm(0, false));
                as.movne(R0(), imm(1, false));
                as.label(end2);
                return false;
        }
        else if(bin.getOp().equals("&&"))
        {
            res1=compile(bin.getLhs());
                as.mov(R1(), R0());
            Label end3=new Label();
                as.mov(R0(), imm(1, false));
                as.cmp(R1(), imm(0, false));
                as.moveq(R0(), imm(0, false));
                as.cmp(R0(), imm(1, false));
                as.bne(end3);
                res2=compile(bin.getRhs());
                as.mov(R2(), R0());
                as.cmp(R2(), imm(0, false));
                as.moveq(R0(), imm(0, false));
                as.label(end3);
                return false;
        }
        res1 = compile(bin.getRhs());
        as.push(R0());
        res2 = compile(bin.getLhs());
        as.mov(R1(), R0());
        as.pop(R2());
        result = res1 || res2;
        switch (bin.getOp()) {
            case "+":
                if (result) {
                    if (!res1) {
                        as.mov(R7(), R1());
                        as.mov(R0(), R2());
                        i2f();
                        as.mov(R2(), R0());
                        as.mov(R1(), R7());
                    }
                    if (!res2) {
                        as.mov(R7(), R2());
                        as.mov(R0(), R1());
                        i2f();
                        as.mov(R1(), R0());
                        as.mov(R2(), R7());
                    }
                    as.mov(R0(), R2());
                    fadd();
                } else {
                    as.add(R0(), R1(), R2());
                }
                break;
            case "-":
                if (result) {
                    if (!res1) {
                        as.mov(R7(), R1());
                        as.mov(R0(), R2());
                        i2f();
                        as.mov(R2(), R0());
                        as.mov(R1(), R7());
                    }
                    if (!res2) {
                        as.mov(R7(), R2());
                        as.mov(R0(), R1());
                        i2f();
                        as.mov(R1(), R0());
                        as.mov(R2(), R7());
                    }
                    as.mov(R0(), R1());
                    as.mov(R1(), R2());
                    fsub();
                } else {
                    as.sub(R0(), R1(), R2());
                }
                break;
            case "*":
                if (result) {
                    if (!res1) {
                        as.mov(R7(), R1());
                        as.mov(R0(), R2());
                        i2f();
                        as.mov(R2(), R0());
                        as.mov(R1(), R7());
                    }
                    if (!res2) {
                        as.mov(R7(), R2());
                        as.mov(R0(), R1());
                        i2f();
                        as.mov(R1(), R0());
                        as.mov(R2(), R7());
                    }
                    as.mov(R0(), R1());
                    as.mov(R1(), R2());
                    fmul();
                } else {
                    as.mul(R0(), R1(), R2());
                }
                break;
            case "/":
            case "%":
                if (result) {
                    if (bin.getOp().equals("%")) {
                        // error
                        break;
                    }
                    if (!res1) {
                        as.mov(R7(), R1());
                        as.mov(R0(), R2());
                        i2f();
                        as.mov(R2(), R0());
                        as.mov(R1(), R7());
                    }
                    if (!res2) {
                        as.mov(R7(), R2());
                        as.mov(R0(), R1());
                        i2f();
                        as.mov(R1(), R0());
                        as.mov(R2(), R7());
                    }
                    as.mov(R0(), R1());
                    as.mov(R1(), R2());
                    fdiv();
                } else {
                    Label loop = new Label();
                    Label end = new Label();
                    as.mov(R0(), imm(0, false));
                    as.mov(R3(), imm(0, false));
                    as.mov(R4(), imm(0, false));
                    as.mov(R5(), imm(1, false));
                    as.cmp(R1(), imm(0, false));
                    as.sublt(R1(), R0(), R1());
                    as.addlt(R4(), R4(), imm(1, false));
                    as.sublt(R5(), R0(), R5());
                    as.cmp(R2(), imm(0, false));
                    as.sublt(R2(), R0(), R2());
                    as.sublt(R4(), R4(), imm(1, false));
                    as.jmp(loop);
                    as.label(loop);
                    as.cmp(R1(), R2());
                    as.addge(R3(), R3(), imm(1, false));
                    as.jmplt(end);
                    as.sub(R1(), R1(), R2());
                    as.jmp(loop);
                    as.label(end);
                    if (bin.getOp().equals("/")) {
                        as.cmp(R4(), imm(0, false));
                        as.subne(R3(), R0(), R3());
                        as.mov(R0(), R3());
                    } else if (bin.getOp().equals("%")) {
                        as.mul(R0(), R1(), R5());
                    }
                }
                break;
            case "==":
                as.cmp(R1(), R2());
                as.mov(R0(), imm(0, false));
                as.moveq(R0(), imm(1, false));
                return false;
            case ">":
                as.cmp(R1(), R2());
                as.mov(R0(), imm(0, false));
                as.movgt(R0(), imm(1, false));
                return false;
            case "<":
                as.cmp(R1(), R2());
                as.mov(R0(), imm(0, false));
                as.movlt(R0(), imm(1, false));
                return false;
            case ">=":
                as.cmp(R1(), R2());
                as.mov(R0(), imm(0, false));
                as.movge(R0(), imm(1, false));
                return false;
            case "<=":
                as.cmp(R1(), R2());
                as.mov(R0(), imm(0, false));
                as.movle(R0(), imm(1, false));
                return false;
            default:// error
                throw new Error("Unknown operator " + bin.getOp());
        }
        return result;
    }

    public boolean visit(Assign assign) {
        Var var = (Var) assign.getLhs();
        boolean result = false;
        if (var.isArray()) {
            if(var.isGloabl()){
                as.ldr(R1(), var.getMemRef());
                caculateIndex(var.getShape(),var.getIndex());
                as.add(R0(), R0(), R1());
                as.push(R0());
            }
            else{
                caculateIndex(var.getShape(),var.getIndex());
                if(var.isParam){
                    as.ldr(R5(), new IndirectMemoryReference(var.getMemRef().getOffset(), FP()));
                    as.add(R0(), R0(), R5());
                }
                else
                {
                    as.add(R0(), R0(), imm(var.getMemRef().getOffset(), false));
                    as.add(R0(), R0(), FP());
                }
                as.push(R0());
                // as.pop(R1());
                // as.str(R1(),new IndirectMemoryReference(0, R0()));
            }
            var.Init();
            boolean isFloat = compile(assign.getRhs());
            if (var.getType().equals("float")) {
                result = true;
                if (!isFloat) {
                    i2f();
                }
            } else if (isFloat) {
                f2i();
            }
            as.pop(R1());
            as.str(R0(), new IndirectMemoryReference(0, R1()));
            // TODO:waiting for array
        } else {
            boolean isFloat = compile(assign.getRhs());
            if (var.getType().equals("float")) {
                result = true;
                if (!isFloat) {
                    i2f();
                }
            } else if (isFloat) {
                f2i();
            }
            
            if(var.isGloabl())
            {
                as.ldr(R1(), var.getMemRef());
                as.str(R0(), new IndirectMemoryReference(0, R1()));
            }
            else
                {
                    as.str(R0(), var.getMemRef());
                }
            var.Init();
        }
        return result;
    }

    public boolean visit(Var var) {
        boolean result = false;
        if (var.getType().equals("float") && !var.isArray()) {
            result = true;
        }
        if (!var.isInited()) {
            if (var.getExpr() == null) {
                if(var.isArray()){
                    if(var.getValue_list()!=null)
                    for(int i = 0; i < var.getArraySize(); i++){
                        as.mov(R0(), imm(var.getValue_list().get(i).getIntValue(), false));
                        as.str(R0(), new IndirectMemoryReference(i*4+var.getMemRef().getOffset(), FP()));
                    }
                    var.Init();
                    return true;
                }
                return result;
            }
            boolean isFloat = compile(var.getExpr());
            if (var.getType().equals("float") && !isFloat) {
                i2f();
            }
            as.str(R0(), var.getMemRef());
            var.Init();
        } else {
            if (var.isGloabl()) {
                
                as.ldr(R1(), var.getMemRef());
                if(var.isArray())
                    {
                        caculateIndex(var.getShape(),var.getIndex());
                        as.add(R0(), R0(), R1());
                        as.ldr(R0(), new IndirectMemoryReference(0, R0()));
                    }
                else
                    as.ldr(R0(), new IndirectMemoryReference(0, R1()));
            } else {
                if(var.isArray())
                {   
                    if(var.isParam){
                        as.ldr(R1(), var.getMemRef());
                        caculateIndex(var.getShape(),var.getIndex());
                        as.add(R0(), R0(), R1());
                        as.ldr(R0(), new IndirectMemoryReference(0, R0()));
                    }
                    else{
                        int index=caculateIndex(var.getShape(),var.getIndex());
                        if(index==0)
                            as.add(R0(),FP(),imm(var.getMemRef().getOffset(), false));
                        else
                            {
                                as.add(R0(), R0(), imm(var.getMemRef().getOffset(), false));
                                as.add(R0(), R0(), FP());
                                as.ldr(R0(), new IndirectMemoryReference(0, R0()));
                            }
                    }
                }
                else
                    as.ldr(R0(), var.getMemRef());
            }
        }
        return result;
    }

    private void i2f() {
        Symbol sym = namedSymbol("__aeabi_i2f");
        as.call(new DirectMemoryReference(sym));
    }

    private void f2i() {
        Symbol sym = namedSymbol("__aeabi_f2i");
        as.call(new DirectMemoryReference(sym));
    }

    private void fadd() {
        Symbol sym = namedSymbol("__aeabi_fadd");
        as.call(new DirectMemoryReference(sym));
    }

    private void fsub() {
        Symbol sym = namedSymbol("__aeabi_fsub");
        as.call(new DirectMemoryReference(sym));
    }

    private void fdiv() {
        Symbol sym = namedSymbol("__aeabi_fdiv");
        as.call(new DirectMemoryReference(sym));
    }

    private void fmul() {
        Symbol sym = namedSymbol("__aeabi_fmul");
        as.call(new DirectMemoryReference(sym));
    }

    private Register R0() {
        return createRegister(RegisterType.R0);
    }

    private Register R1() {
        return createRegister(RegisterType.R1);
    }

    private Register R2() {
        return createRegister(RegisterType.R2);
    }

    private Register R3() {
        return createRegister(RegisterType.R3);
    }

    private Register R4() {
        return createRegister(RegisterType.R4);
    }

    private Register R5() {
        return createRegister(RegisterType.R5);
    }

    private Register R6() {
        return createRegister(RegisterType.R6);
    }

    private Register R7() {
        return createRegister(RegisterType.R7);
    }

    private Register R8() {
        return createRegister(RegisterType.R8);
    }

    private Register R9() {
        return createRegister(RegisterType.R9);
    }

    private Register R10() {
        return createRegister(RegisterType.R10);
    }

    private Register FP() {
        return createRegister(RegisterType.fp);
    }

    private Register IP() {
        return createRegister(RegisterType.ip);
    }

    private Register SP() {
        return createRegister(RegisterType.sp);
    }

    private Register LR() {
        return createRegister(RegisterType.lr);
    }

    private Register PC() {
        return createRegister(RegisterType.pc);
    }

    private int caculateIndex(List<Expr>shape,List<Expr> exprs){
        int x=0;
        as.mov(R5(), imm(0, false));
        for(Expr expr:exprs){
            System.out.println(expr);
            compile(expr);
            int t=1;
            if(shape!=null)
            for(int j=x+1;j<exprs.size();j++){
                t=t*shape.get(j).getIntValue();
            }
            else
                throw new Error("shape is null");
            as.mov(R6(), imm(t, false));
            as.mul(R0(), R0(), R6());
            as.add(R5(), R0(), R5());
            x++;
        }
        as.mov(R0(), R5());
        as.mov(R5(), imm(4, false));
        as.mul(R0(), R0(), R5());
        return x;
    }
}